/**
 * 
 */
package prj3;

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import prj3.exceptions.EmptyListException;
import util.KeplerLaws;

/**
 * Implements the Earth. It will add the rotation aspect of the
 * Earth as well as the Sun's influence
 * @author romain
 *
 */
public class EarthGrid extends SphericalGrid implements Runnable, ISimulate {

	/**
	 * Value in degrees Celcius of the heat received per square meter and per
	 * minute from the sun when the Earth is at the perihelion
	 */
	public final static float HEAT_PER_SQUARE_METER_PER_MINUTE = 0.000001f;
	
	/**
	 * @param radius
	 * @param step
	 */
	public EarthGrid(float radius, float step, float excentric, float maxTilt) {
		this(radius, step, step, excentric, maxTilt);
	}

	/**
	 * @param _radius
	 * @param _stepTheta
	 * @param _stepPhi
	 */
	public EarthGrid(float _radius, float _stepTheta, float _stepPhi, float _excentricity, float _maxTilt) {

		super(_radius, _stepTheta, _stepPhi);
		
		/* Now we create the cells with the given spacing */
		cellMap = new HashMap();
		for(int longi=0; longi < maxLongitude; longi++) {
			for(int lati=1; lati <= maxLatitude; lati++) {
				SphericalCoordinate tC = new SphericalCoordinate(this, radius, longi, lati);
				cellMap.put(tC, new EarthCell(this, tC));
			}
		}
		
		sunAngle = 0;
		excentricity = _excentricity;
		maxTilt = _maxTilt;
		kLaws = new KeplerLaws(util.Constants.startDate.get(java.util.GregorianCalendar.DAY_OF_MONTH), 
				util.Constants.startDate.get(java.util.GregorianCalendar.MONTH), excentricity, maxTilt);
        
		isFullInit = false;
		myAlgorithm = new EarthAlgorithm(this);
	}
	
	
	public void stepGrid() {
		// We will step by a default of 1 minute in this case
		if(!isFullInit)
			fullyInit();
		stepGrid(1);
	}

	/**
	 * Steps the grid
	 * @param chMinutes
	 */
	public void stepGrid(int chMinutes) {
		if(!isFullInit)
			fullyInit();
		
        if(chMinutes < 0 || chMinutes > 525600)
            throw new IllegalArgumentException("StepGrid can only step from 1 minute to 1 day");
         
        
       kLaws.step(chMinutes);
       currentTilt = kLaws.getCurrentTilt();
       distance = kLaws.getDistance();
        
        System.out.println("Tilt is " + currentTilt + " and distance is " + distance);
        
        sunAngle = kLaws.getSunAngle();
		
        while(!setSunAngle(sunAngle)){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        while(!setSunDistance((float)distance)){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        while(!setSunLatitude(currentTilt)){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        while(!setSunOrbitAngle(kLaws.getRealAnomaly())){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
		/* Now calculate the heating addition for each cell */
		/* We will iterate over all cells */
		SphericalCoordinate curCoord;
		EarthCell curCell;
		boolean curMutable;
		
		float totalHeat = 0;
		
		Iterator it = cellMap.entrySet().iterator();
		while(it.hasNext()) {
			Map.Entry pair = (Map.Entry)it.next();
			curCoord = (SphericalCoordinate)pair.getKey();
			curCell = (EarthCell)pair.getValue();
			curMutable = curCell.isMutable();
			curCell.setMutable(true);
			double longiAngle = curCoord.getPhi() - sunAngle;
			// Get the angle between -PI and PI
			if(longiAngle <= -Math.PI) {
				longiAngle += 2*Math.PI;
			}
			if(longiAngle >= Math.PI) {
				longiAngle -= 2*Math.PI;
			}
			
			/* To make sure we don't heat too much,
			 * we only consider cells that are FULLY in the sun
			 */
			if(longiAngle + this.stepPhi <= -Math.PI/2 || longiAngle > Math.PI/2) {
				// On the dark side
				curCell.setExtraHeating(0);
			} else {
				// We calculate attenuation
				float attenuation = (float)Math.abs(Math.cos(longiAngle + stepPhi/2.0)); // For longitude aspect
				
				// Now for lattitude
				attenuation *= (float)Math.abs(Math.cos(Math.PI/2 - currentTilt - curCoord.getTheta() + stepTheta/2.0));
				
				// Now for the distance
				attenuation *= Math.pow(kLaws.getPerihelionDistance(), 2)/Math.pow(distance, 2);
				
				/* Approximation: this will be false if chMinutes is too
				 * big, we need to do an integration in that case but
				 * we will simplify
				 */
				curCell.setExtraHeating(attenuation*heat_sqm_minute*
						curCell.getArea()*chMinutes);
				totalHeat += attenuation*heat_sqm_minute*curCell.getArea();
			}
			curCell.setMutable(curMutable);
		}
		EarthCell.setCooling((totalHeat)/totalArea*chMinutes);
		/* We now update the grid */
		myAlgorithm.updateGrid();
        
        simulatedDateTime.add(Calendar.MINUTE, chMinutes); // Important to be last
        
        if(!setSimulatedTimeStep(simulatedDateTime)){
            //System.out.println("BufferFull");
            try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
            return;
        }
		
	}
    
    public double getEccentricity()
    {
		if(!isFullInit)
			fullyInit();
		return excentricity;
    }
    
    public double getMaximumTilt()
    {
		if(!isFullInit)
			fullyInit();
    	return maxTilt;
    }
   

	/* Protected values */
	protected double sunAngle; // [0-2PI[: Above which "longitude" is the sun on
	protected float heat_sqm_minute;
	protected float totalArea;
	protected EarthAlgorithm myAlgorithm;
	protected boolean isFullInit;
	protected float coolMultiplier = 1f;
	
	protected util.KeplerLaws kLaws;
	protected double excentricity;
	protected double distance;
    protected double currentTilt; // Tilt in radians
    protected double maxTilt; // Maximum tilt in radians
    protected double sunOrbitAngle;
	
    
    protected synchronized void fullyInit() {
		initHeatingCooling();
		
		// Initialize all other data
		distance = kLaws.getDistance();
		currentTilt = kLaws.getCurrentTilt();
		
        while(!setSimulatedTimeStep(simulatedDateTime)){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        while(!setSunAngle(sunAngle)){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        while(!setSunDistance((float)distance)){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        while(!setSunLatitude(currentTilt)){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        while(!setSunOrbitAngle(kLaws.getRealAnomaly())){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        isFullInit = true;
    }
    
	protected void initHeatingCooling() {
		heat_sqm_minute = HEAT_PER_SQUARE_METER_PER_MINUTE;
		
		totalArea = 0;
		
		for(int longi=0; longi < maxLongitude; longi++) {
			for(int lati=1; lati <= maxLatitude; lati++) {
				SphericalCoordinate tC = new SphericalCoordinate(this, radius, longi, lati);
				EarthCell cell = (EarthCell)this.getCellForCoordinate(tC);
				totalArea += cell.getArea();
			}
		}
	}

    
    /**
     * Set the simulated timeStep.  
     * Should be called whenever the timestep is updated so it can be buffered if necessary.
     * @param timeStep
     * @return
     */
    public boolean setSimulatedTimeStep(GregorianCalendar timeStep){
        this.simulatedDateTime = timeStep;
        return true;
    }
    
    /**
     * Perform a destructive get of the Simulated Time Step (if applicable)
     * @return
     * @throws EmptyListException
     */
    public GregorianCalendar takeSimulatedTimeStep() throws EmptyListException {
		if(!isFullInit)
			fullyInit();
        return simulatedDateTime;
    }
    
    public GregorianCalendar getSimulatedTimeStep(int position) throws EmptyListException {
		if(!isFullInit)
			fullyInit();
    	return simulatedDateTime;
    }
    
    /**
     * Set the sun Angle.  
     * Should be called whenever the sunangle is updated so it can be buffered if necessary.
     * @param timeStep
     * @return
     */
    public boolean setSunAngle(double sunAngle){
        this.sunAngle = sunAngle;
        return true;
    }
   
    /**
     * Perform a destructive get of the sun Angle (if applicable)
     * @return
     * @throws EmptyListException
     */
    public double takeSunAngle() throws EmptyListException {
		if(!isFullInit)
			fullyInit();
    	return sunAngle;
    }

	/* (non-Javadoc)
	 * @see prj3.ISimulate#setSunDistance(float)
	 */
	public boolean setSunDistance(double sunDistance) {
		this.distance = sunDistance;
		return true;
	}

	/* (non-Javadoc)
	 * @see prj3.ISimulate#setSunLatitude(float)
	 */
	public boolean setSunLatitude(double sunAngle) {
		this.currentTilt = sunAngle;
		return true;
	}

	/* (non-Javadoc)
	 * @see prj3.ISimulate#setSunOrbitAngle(float)
	 */
	public boolean setSunOrbitAngle(double sunAngle) {
		sunOrbitAngle = sunAngle;
		return true;
	}

	/* (non-Javadoc)
	 * @see prj3.ISimulate#takeSunDistance()
	 */
	public double takeSunDistance() throws EmptyListException {
		if(!isFullInit)
			fullyInit();
		return distance;
	}

	/* (non-Javadoc)
	 * @see prj3.ISimulate#takeSunLatitude()
	 */
	public double takeSunLatitude() throws EmptyListException {
		if(!isFullInit)
			fullyInit();
		return currentTilt;
	}

	/* (non-Javadoc)
	 * @see prj3.ISimulate#takeSunOrbitAngle()
	 */
	public double takeSunOrbitAngle() throws EmptyListException {
		if(!isFullInit)
			fullyInit();
		return sunOrbitAngle;
	}
    
    
}
